# 👉 手把手教你发个 npm 包
# 什么是 npm 包?
npm 是包管理工具,是前端模块化下的一个标志性产物。那什么是包呢?🍞 ?👜 ?❌ !node.js 中的第三方模块又叫做包,包是由第三方个人或团队开发出来的,供其他开发者使用的模块代码。通过 npm 这个包管理工具创建发布包和下载包,复用包里面的工具代码,从而提高工作效率。
本文将会通过实现一个版本比较的小功能,手把手教如何发一个 npm 包。
包含哪些内容?
- 发布一个 Typescript 包,包含函数声明文件
- rollup 打包,支持 COMMONJS / UMD / ES Module 格式
- ESlint 校验, commit message 格式约束
- 发包前自动升级版本并构建
- 一些关于 npm 包的小 tips
# 准备
# 注册账号
发布包之前你必须拥有一个 npm 的账号,戳 https://www.npmjs.com/ 官网链接,注册一个账号。
# 创建项目
创建项目,初始化项目基本结构和更新 package.json
文件,用于描述当前项目的功能。
mkdir compare_version
cd compare_version
接着初始化 npm 包的一些配置:
npm init
// 接下过程是以问答式 CLI 方式进行
// Q1:package name: (文件夹名字) | 默认会以文件夹名字命名,当然也可以自定义包名,但需要遵守 npm 命名规范
// Q2:version: (1.0.0) | 版本号,默认是 1.0.0,具体版本号规则,可参考《语义化版本 2.0.0》
// Q3:description: | 包描述,用于描述这个包的主要功能以及用途
// Q4:entry point: (index.js) | 入口文件,即从那个文件开始执行
// Q5:test command: | 测试命令,用于包测试的命令,可以后续补充关于测试包的编写
// Q6:git repository: | 项目的 git 存储库地址
// Q7:keywords: | 描述包的关键字,用于在 npmjs 上查询关键字
// Q8:author: | 作者名字,可以使用 npmjs 名称、npmjs 注册邮箱、github 注册邮箱
// Q9:license: (ISC) | 开源协议,默认 ISC
// Q10:输出 package.js 内容并询问 Is this OK? (yes) 默认为 yes,回车后生成文件
在初始化的 package.json
文件中,它包含了你的项目信息以及众多配置项。除此之外,也可以写一个 README.md
文件用来描述你的项目。这里举个 package.json
的具体配置例子:
{
"name": "compare_version_lib",
"version": "1.0.0",
"description": "This is a js library that help you to compare two version number.",
"main": "dist/index.umd.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts",
"scripts": {
"dev": "rollup -w -c",
"build": "rollup -c",
"test": "jest",
"prepare": "husky install"
},
"keywords": [],
"author": "chieminchan",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/chieminchan/compare_version_lib.git"
},
"files": ["src/", "dist/"],
"devDependencies": {}
}
- files 字段是用于约定在发包的时候 NPM 会发布包含的文件和文件夹。注意: files 字段中文件夹名直接写名字,不要包含 ./ 字符,否则打包出来的产物不会包含该文件夹。
# npm 包命名机制
在 npm 的包管理系统中,有一种 scoped packages 机制,用于将一些 packages 以 @scope/package
的命名形式,让同个域级的包集中在一个命名空间下面,实现域级的包管理,同时还能避免包名冲突的情况。
npm view packageName
查看包是否被占用,并可以查看它的一些基本信息,若包名称从未被使用过,则会抛出 404 错误。在初始化项目时,可以使用命令行来添加 scope:
npm init --scope=scopeName
同域级范围内的包会被安装在相同的文件路径下
node_modules/@scopeName/
: 562例如:vue 框架里面涉及的一些脚手架工具包:@vue/cli-plugin-babel、@vue/cli-plugin-eslint 等等域级包。
# 初始化 Typescript 环境
我们打包给用户,在用户引用时,还需要支持在 TS 环境下的代码提示,所以再来快速初始化生成一个 tsconfig.json
文件,该文件属于 Typescript 配置文件。
npm add typescript -D
tsc --init
按需配置tsconfig.json
文件:
{
"compilerOptions": {
"rootDir": "./src",
"declaration": true,
"declarationDir": "./dist",
"outDir": "./dist",
"target": "ES2015",
"module": "ES2020",
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
}
}
- 使用 declaration 会自动生成类型声明文件。配置后,在编译过程中会向
./dist
目录输出index.d.ts
类型声明文件 - target 表示要编译出的代码格式,一般使用 es6
- module 表示源代码的语法版本,这是使用最新的 es2020。
# 引入打包编译工具
包发布前,我们需要将编写的代码打包编译成 js 代码,并进行相关压缩操作。
rollup 是一个 JavaScript 模块打包器,在功能上要完成的事和 webpack 性质是一样的,就是将小块代码编译成大块复杂的代码。在平时开发应用程序时,我们基本上选择用 webpack。这里使用 rollup,相比 webpack 它更轻量,配置少,比较适合工具库的打包。
这里需要先初始化 Rollup 打包环境:
- rollup 安装
npm install rollup -g # 全局安装
npm install rollup -D # 项目本地安装
npm install @rollup/plugin-typescript -D # 将Typescript转换成为 ES6+ 标准
npm install @rollup/plugin-commonjs -D # rollup默认不支持CommonJS,自己写的时候可以尽量避免使用CommonJS模块的语法,但有些外部库的是cjs或者umd(由webpack打包的)。如果使用这些外部库就需要支持CommonJS模块。
npm install @rollup/plugin-node-resolve -D
- 创建配置文件
rollup.config.js
import commonjs from "@rollup/plugin-commonjs";
import typescript from "@rollup/plugin-typescript";
export default {
input: "src/index.ts",
output: [
{
file: "dist/index.esm.js",
format: "es",
},
{
file: "dist/index.umd.js",
format: "umd",
name: "index.umd.js",
},
],
plugins: [commonjs(), typescript()],
};
上述文件表示我们要打包编译出两个文件,分别是 UMD 格式的和 ES Module 格式的。UMD 格式需要定义一个变量,这样在浏览器环境下,所有的方法都会挂载在这个变量上了。
- 增加编译命令
{
"script": {
"dev": "rollup -w -c",
"build": "rollup -c"
}
}
如此,我们便可以通过 npm run dev
,就能在开发的时候实时编译。如果需要打包,则先执行 npm run build
命令即可完成编译打包。
# ESlint 配置
运行初始化配置命令,根据项目情况来设置特地配置:
npm init @eslint/config
其中,会触发安装以下三个依赖:
npm install -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
eslint
:ESLint 的核心代码@typescript-eslint/parser
:ESLint 的解析器,用于解析 typescript。因为 eslint 检测代码的核心原理是,先将 JS 代码生成 AST,然后遍历 AST,在不同类型的节点进行不同规则的批评校验。而 TS 无法直接生成 AST,因此需要先 parser ,才能进一步对 Typescript 代码进行检查和规范@typescript-eslint/eslint-plugin
:这是一个 ESLint 插件,包含了各类定义好的检测 Typescript 代码的规范
最终,项目根目录下会生成.eslintrc.json
文件,该文件中定义 ESLint 的基础配置,比如:一个简单的配置如下:
{
"env": {
"browser": true,
"es2021": true
},
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": ["@typescript-eslint"],
"rules": {}
}
eslint:recommended
,这个配置文件的 rules 部分开启了所有 ESLint 推荐使用的规则- 同时,也可以通过 plugin 插件引入可共享配置。为
plugin:{plugin_pacakge_name}/{config_name}
- extends 允许配置多个模块,如果模块间的配置有冲突,位置靠后的配置会覆盖前面的。
- 个性化的约束规则可以在 rules 进行设置,当 rules 和 extends 中配置了相同规则,rules 中的配置的优先级会高于 extends。
# Husky - Git 提交约束
Eslint 对我们编码格式进行了约束,但难以避免没有按要求进行格式化代码就提交到了远程的情况。为此,引入 Husky,在git commit
提交前进行自动格式化暂存区内文件,以及校验是否符合 Eslint 规则,从而明确统一远程仓库代码的规范。
# Husky 是什么?
在项目的 .git/hooks
文件夹内有很多 Hooks,这些钩子会在 git 执行的某些节点被触发。以一次 commit 为例,会先后触发 pre-commit
、prepare-commit-msg
、commit-msg
和 post-commit
等 hooks。
随机打开其中的 .git/hooks/pre-commit.sample
,可以看到,实质上是一个 shell 脚本。Git 提供了一个自定义 Hooks 文件夹的 config,可以通过设置 core.hooksPath
指向自定义目录,即,我们可以自定义设置 shell 脚本来实现个性化功能。
回到主题上,Husky 就是能够简化创建或者修改 Githooks 过程的工具。
它最新的运作原理大概就是:
husky install
安装时创建 hooks,将git hooks
的目录指定为.husky/
- 提交时从配置文件中(
package.json
、.huskyrc
、.huskyrc.json
...)读取相应的 hook 配置,使用husky add
命令向.husky/
中添加 hook - 特定时机,触发执行配置中的指令/脚本
具体原理结合源码分析可以参考: Husky 原理解析及在代码 Lint 中的应用 (opens new window)
# Husky 配合 lint-staged 食用更佳
- 引入 commitlint,校验提交的 message 格式是否符合规范,以此统一 git commit message。
- 引入 lint-staged,针对当次提交(暂存区的代码)的
ts
和js
文件进行检测。
npm install husky -D
npm install @commitlint/cli -D
npm install @commitlint/config-conventional -D
npm install lint-staged -D
husky 6.0.0
版本以前,安装 lint-staged
后,需要在 package.json
文件中,设置我们需要的 git hooks:
"husky": {
"hooks": {
// 在 commit 之前先,针对lint-staged配置的文件执行配置好的指令
"pre-commit": "lint-staged",
// 校验 commit 时添加的备注信息是否符合要求规范
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
"commitlint": {
"extends": ["@commitlint/config-conventional"]
},
"lint-staged": {
"*.{ts,js}": [
"node --max_old_space_size=8192 ./node_modules/.bin/prettier -w",
"node --max_old_space_size=8192 ./node_modules/.bin/eslint --fix --color",
"git add"
]
}
以上的配置表示开发者在 git commit
时,会首先调用 lint-staged
相关命令: prettier 格式化,然后是 ESlint 校验并修复,然后将修改后的文件存入暂存区,然后是校验 commit message 是否符合规范,符合规范后才会成功 commit。
husky 6.0.0
版本后,不再使用 .huskyrc.js
文件,同时也不支持在 package.json 文件中进行 husky 相关配置,而是在 .husky/
目录中配置的单个 git 钩子。
可以直接使用 npx mrm@2 lint-staged
代替 npm install lint-staged -D
,它会直接在项目中配置好 husky 和 lint-staged,你只需要在 package.json 中修改一下 lint-staged 检测文件范围
% npx mrm@2 lint-staged
npx: 237 安装成功,用时 21.067 秒
Running lint-staged...
Update package.json
husky - Git hooks installed
husky - created .husky/pre-commit
"lint-staged": {
"{*.js,jsx,ts,tsx}": "eslint --cache --fix"
},
# 开发
到这一步,可以开始编写代码啦!
项目基本配置文件外,npm module 里的一般包括三个文件夹:
dist | src | types
- dist 包编译后最终产出文件
- src 源码文件
- types 类型声明文件
新建
src/index.ts
文件
/*
* compare version
* 比较版本号,版本号规则x.y.z,xyz均为大于等于0的整数
* 0: 相等
* 1: 大于
* -1: 小于
*/
/*
* compare version
* 比较版本号,版本号规则x.y.z,xyz均为大于等于0的整数
* 0: 相等
* 1: 大于
* -1: 小于
*/
const COMPARE_RESULT_MAP = {
BIGGER: 1,
SMALLER: -1,
SAME: 0,
};
function versionCompare(version1: string, version2: string): number {
const version1Arr = version1.split(".");
const version2Arr = version2.split(".");
const maxLength = Math.max(version1Arr.length, version2Arr.length);
for (let i = 0; i < maxLength; i++) {
const num1 = +version1Arr[i] || 0;
const num2 = +version2Arr[i] || 0;
if (num1 !== num2) {
return num1 > num2
? COMPARE_RESULT_MAP.BIGGER
: COMPARE_RESULT_MAP.SMALLER;
}
}
return COMPARE_RESULT_MAP.SAME;
}
export default versionCompare;
# 发布
# 设置发布仓库 registry
在下载包时,有些人会偏向于设置 taobao 镜像,因为 npm 仓库服务器在国外,下载速度比较慢。发布的时候也一样,一般开源应用基本都发布到 npmjs,公司内部包的话就会发到私有 npm 仓库,我们可以在 package.json 设置一下你想要发布的仓库地址,默认是执行 publish 命令所指向的仓库:
"publishConfig": {
"registry": "http://registry.npm.xxx.com/"
}
也可以设置别名
// 设置别名
alias ynpm="npm --registry=http://registry.npm.xxx.com"
// 发布
ynpm publish
发布包需要验证你的账号权限,第一次执行需要 npm login
。
# 版本管理
npm 的发包需要遵循语义化版本,一个版本号包含三个部分:major.minor.patch
,
我们可以使用 npm version
命令来自动修改版本号,比如:
// version = v1.0.0
npm version major // 主版本号:当你做了不兼容的API修改
# v2.0.0
npm version minor // 次版本号:当你做了向下兼容的功能性新增,可以理解为Feature版本
# v2.1.0
npm version patch // 修订号:当你做了向下兼容的问题修正,可以理解为Bug fix版本
# v2.1.1
一般来说还有先行版本,测试版本等,先行版本号是加到修订号后面,作为版本号的延伸;版本中携带 alpha
、beta
、rc
等 tag 字样,统称先行版,带有预发布版本号的,一般格式为 x.y.z-[tag].[次数 / meta 信息]
。比如:3.1.0-beta.0
、3.1.0-alpha.0
当要发行大版本或核心功能时,但不能保证这个版本完全正常,就要先发一个先行版本。我们可以使用 --preid 参数来发布先行版本,: npm version prerelease --preid=alpha
。
npm version premajor // 预备主版本
# v3.0.0-0
npm version preminor // 预备次版本
# v3.1.0-0
npm version prepatch // 预备修订版本
# v3.1.1-0
npm version prerelease // 预发布版本
# v3.1.1-1
npm version prerelease --preid=alpha // 命名先行版,测试版用beta
# v3.1.1-alpha.0
npm version prerelease // 升级先行版
# v3.1.1-alpha.1
运行 npm version
时,本地 git status
需要是 clear 的,因为运行命令之后会自动提交一个 commit 并打上 tag,也可以自定义 commit message。
npm version patch -m "升级到 %s: 演示自定义msg"
# v3.1.1
# Changelog
包发布了很多次后,使用者升级就需要知道他是否需要升级,需要查看文档看看有哪些不兼容性改动,所以需要一个 Changelog 来记录每次发布改了些什么。
如果手动的维护肯定会有忘记的时候,所以需要使用工具来自动生成,我们可以使用 standard-version
这个包来生成,这个包的作用是自动更新版本和生成 CHANGELOG。
npm install --D standard-version # 安装 standard-version
将 npm run 脚本添加到您的 package.json:
{
"scripts": {
"patch": "standard-version --patch",
"release": "standard-version",
"release:alpha": "standard-version --prerelease alpha"
}
}
现在就可以使用 npm run release --patch
代替 npm version
。
% npm run patch
> compare_version_lib@1.1.1 patch /compare_version
> standard-version --patch
✔ bumping version in package.json from 1.1.1 to 1.1.2
✔ bumping version in package-lock.json from 1.1.1 to 1.1.2
✔ outputting changes to CHANGELOG.md
✔ committing package-lock.json and package.json and CHANGELOG.md
→ No staged files match any configured task.
✔ tagging release v1.1.2
ℹ Run `git push --follow-tags origin master && npm publish` to publish
// 会根据每个commit生成Changelog:
# Changelog
### [1.1.2](https://github.com/chieminchan/compare_version_lib/compare/v1.1.1...v1.1.2) (2022-03-21)
### Features
- update srcript ([2ea0d93](https://github.com/chieminchan/compare_version_lib/commit/2ea0d9369ed6c1019bc99a5fc1b2685727def39c))
# TIPS:
# package.json 的依赖版本号
在 package.json 的一些依赖的版本号中,我们还会看到^、~或者>=这样的标识符,或者不带标识符的,这都代表什么意思呢?
没有任何符号:完全百分百匹配,必须使用当前版本号对比符号类的:>(大于) >=(大于等于) <(小于) <=(小于等于) 波浪符号~:固定主版本号和次版本号,修订号可以随意更改,例如~2.0.0,可以使用 2.0.0、2.0.2 、2.0.9 的版本。插入符号^:固定主版本号,次版本号和修订号可以随意更改,例如^2.0.0,可以使用 2.0.1、2.2.2 、2.9.9 的版本。任意版本*:对版本没有限制,一般不用或符号:||可以用来设置多个版本号限制规则,例如 >= 3.0.0 || <= 1.0.0
- 通过 jsdelivr 可以方便的查看包内容
- np 包,进行不同源的 npm 包发布管理的
参考文章:
- https://juejin.cn/post/6844903870678695943#heading-0
- https://zhuanlan.zhihu.com/p/366786798
← 👉 rollup 入门篇 基础语法篇 →